34장. 문서 없는 레거시를 Agent와 함께 읽기 — Entry Point에서 Side Effect까지
8장에서 첫날을 보냈다.
CLAUDE.md 초안과 금지 목록을 만들었고,
개략적인 구조를 파악했다.
이제 본격적인 조사다.
목적이 달라졌기 때문이다.
8장의 목적: Agent가 일할 수 있게 만들기
34장의 목적: 경계를 그을 수 있을 만큼 이해하기
레거시를 읽는 일이 어려운 이유
| 신규 코드 | 레거시 |
|---|---|
| 의도가 코드에 있다 | 의도가 사라졌다 |
| 쓰는 코드만 있다 | 죽은 코드가 섞여 있다 |
| 한 가지 방식 | 세 세대의 방식이 공존 |
⚠️ 뒤의 두 줄이 Agent에게 특히 위험하다.
Agent는 읽은 코드를 모두 유효한 것으로 취급하는데,
레거시에서는 그 절반이 유효하지 않다.
12장의 Context Pollution이 여기서 실제 피해가 된다.
그래서 조사 단계에서 무엇이 살아 있는지를 함께 확인해야 한다.
진입점을 전수 조사한다
모든 흐름은 진입점에서 시작한다.
그런데 진입점은 Controller만이 아니다.
이 프로젝트의 진입점을 전부 찾아줘. 종류별로 나눠서.
- HTTP: @RestController, @Controller
- 메시지: @KafkaListener, @RabbitListener, @SqsListener
- 스케줄러: @Scheduled, Quartz Job
- 배치: Spring Batch Job, main 함수를 가진 클래스
- 이벤트: @EventListener, @TransactionalEventListener
- 관리자: 별도 포트나 경로로 열린 것
- 웹훅: 외부에서 호출하는 콜백 엔드포인트
각각 개수와 파일 위치를 표로 만들어줘.
🔥 이 목록에서 대개 놀란다.
Controller 40개는 알고 있었는데
스케줄러 17개와 이벤트 리스너 23개는 몰랐던 경우가 흔하다.
그리고 8장에서 만든 CLAUDE.md 의 구조 설명이
얼마나 부실했는지도 여기서 드러난다.
흐름 하나를 끝까지 따라간다
목록이 나오면 그중 하나를 정해 파고든다.
POST /orders/{id}/cancel 요청이 들어왔을 때
실제로 실행되는 코드를 순서대로 따라가줘.
- 각 단계의 파일과 메서드를 적어줘
- 조건에 따라 갈라지는 지점을 표시해줘
- 트랜잭션이 시작되고 끝나는 지점을 표시해줘
- 비동기로 넘어가는 지점을 표시해줘
마지막 두 요구가 백엔드 조사의 핵심이다.
OrderCancelController.cancel()
└─ [TX 시작] OrderCancelFacade.cancel()
├─ OrderService.validateCancellable()
│ └─ 분기: 배송 상태에 따라 예외
├─ PaymentCancelHandler.cancel()
│ └─ [외부] PgClient.cancel() ← TX 안에서 외부 호출
├─ PointRefundService.refund()
└─ [이벤트 발행] OrderCancelledEvent
[TX 종료]
└─ [비동기] NotificationListener.handle()
이 한 장이 나오면
28장에서 다룬 문제(트랜잭션 안 외부 호출)가 눈에 보인다.
조사가 곧 문제 발견이다.
Side Effect를 추적한다
이 절이 이 장에서 가장 중요하다.
리팩터링할 때 실제로 위험한 것은
계산 로직이 아니라 부작용이다.
주문 취소 흐름에서 발생하는 모든 Side Effect를 찾아줘.
- DB 쓰기 (어느 테이블에 무엇을)
- 외부 API 호출
- 메시지·이벤트 발행
- 캐시 조작
- 파일 쓰기
- 알림 발송
각각이 트랜잭션 안인지 밖인지도 표시해줘.
결과는 이런 표가 된다.
| Side Effect | 대상 | 트랜잭션 |
|---|---|---|
| DB 쓰기 | orders, payments, point_histories | 안 |
| 외부 호출 | PG 취소 API | 안 ⚠️ |
| 이벤트 발행 | OrderCancelledEvent | 안 |
| 알림 | 카카오 알림톡 | 밖 (리스너) |
| 캐시 | 주문 요약 캐시 삭제 | 밖 |
⚠️ 이 표가 없으면 리팩터링에서 사고가 난다.
메서드를 옮기다가 알림 발송이 두 번 일어나거나,
캐시 무효화가 빠지는 식이다.
31장에서 본 그 문제다.
죽은 코드를 판별한다
레거시 조사에서 가장 조심할 부분이다.
이 클래스를 호출하는 곳이 없어 보입니다. 삭제 가능합니다.
🔥 Agent의 이 판정을 그대로 믿으면 안 된다.
정적 분석으로 잡히지 않는 호출 경로가 있다.
| 놓치는 경로 | 예 |
|---|---|
| 리플렉션 | Bean 이름 문자열로 조회 |
| 스케줄러 | cron 설정이 DB나 외부에 있음 |
| 프로파일별 활성화 | @Profile("batch") |
| 설정 기반 분기 | 피처 플래그 |
| 외부 시스템 호출 | 다른 서비스가 직접 호출 |
| 문자열 기반 라우팅 | 이벤트 타입 문자열 매핑 |
그래서 판정을 이렇게 나눈다.
사용되지 않아 보이는 클래스를 찾아줘.
단, 다음을 함께 확인하고 판정 근거를 적어줘:
- 리플렉션이나 문자열로 참조되는지
- @Profile, @ConditionalOn* 이 붙어 있는지
- 설정 파일에서 클래스명이 언급되는지
"확실히 미사용" 과 "확인 필요" 로 나눠서 보여줘.
그리고 실제 삭제 판단은
운영 로그나 APM 지표로 확인한 뒤에 한다.
정적으로는 “호출하는 코드가 없다” 까지만 알 수 있고,
“실행되지 않는다” 는 운영 데이터가 말해준다.
git log 를 읽힌다
코드에 없는 정보를 얻는 방법이다.
9장에서 판단을 뒤집었던 그 방법의 확장이다.
최근 1년 커밋 통계를 뽑아줘.
- 변경 빈도가 높은 파일 상위 20개
- 1년 이상 변경되지 않은 패키지
- 한 커밋에서 자주 함께 바뀌는 파일 쌍
| 발견 | 의미 |
|---|---|
| 변경이 잦다 | 활발한 영역. 리팩터링 효과가 크다 |
| 1년째 그대로 | 안정 또는 죽은 코드 |
| 항상 함께 바뀐다 | 🔥 경계가 잘못 그어져 있다 |
세 번째가 8부의 직접적인 입력이 된다.
order 와 point 의 파일이 늘 함께 바뀐다면
두 도메인 사이의 경계가 실제로는 작동하지 않는 것이다.
조사 결과는 문서로
19장의 원칙이다.
지금까지 조사한 것을 docs/order-cancel-flow.md 에 정리해줘.
- 진입점과 호출 흐름
- Side Effect 표
- 확인한 사실과 확인하지 못한 것을 구분해서
- 조사 과정에서 발견한 이상한 점도 별도 절로
마지막 항목이 유용하다.
조사 중에 눈에 걸린 것들은
그 자리에서 고치지 않고 모아둔다.
## 발견한 이상한 점
- PG 취소가 트랜잭션 안에서 호출됨 (28장 규칙 위반)
- AdminOrderService 는 Facade를 거치지 않고 Service 직접 호출
- 부분 취소 횟수 제한이 세 곳에서 다르게 구현됨
이 목록이 나중에 티켓이 된다.
이 장의 핵심
- 8장의 목적은 Agent를 일하게 하는 것, 이 장의 목적은 경계를 그을 만큼 이해하는 것이다
- 레거시에는 죽은 코드와 여러 세대의 방식이 섞여 있다
- 진입점은 Controller만이 아니다 — 스케줄러·리스너·배치·웹훅까지 전수 조사한다
- 흐름을 따라갈 때 트랜잭션 경계와 비동기 전환 지점을 함께 표시하게 한다
- 리팩터링에서 위험한 것은 계산 로직이 아니라 Side Effect다
- Agent의 “미사용” 판정을 그대로 믿지 않는다 — 리플렉션·프로파일·설정 기반 호출이 있다
- 정적으로는 “호출하는 코드가 없다” 까지만 알 수 있다
- 함께 자주 바뀌는 파일 쌍은 경계가 잘못 그어졌다는 신호다
- 조사 중 발견한 이상한 점은 고치지 말고 모아서 티켓으로 만든다